/* Task.cpp
Copyright (c) 2008 Michael Zahniser
Please note the license terms (MIT license) at the end of this document.

Function definitions for the Task class.
*/

#include "Task.h"

#include <ctime>
#include <algorithm>
#include <sstream>

using std::time;
using std::time_t;
using std::localtime;
using std::tm;
using std::getline;
using std::search;
using std::ostringstream;
using std::istringstream;
using std::endl;

namespace {
	static const string MONTH[12] = {
		"Jan", "Feb", "Mar", "Apr", "May", "Jun", 
		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
	};
}

// Temporary hack to allow reading files in the old format.
bool Task::READ_LEGACY = false;



// Default constructor:
Task::Task()
	: flags(0), depth(0)
{
	ClearDate();
}



// Constructor:
// Reads the task from an input stream.
Task::Task(istream &in)
{
	in >> *this;
}



// Copy constructor:
Task::Task(const Task &task)
	: flags(task.flags), depth(task.depth), 
	year(task.year), month(task.month), day(task.day),
	text(task.text)
{	
}



// operator=:
Task &Task::operator=(const Task &task)
{
	flags = task.flags;
	depth = task.depth;
	year = task.year;
	month = task.month;
	day = task.day;
	text = task.text;

	return *this;
}



// DateIsBefore:
// Returns true if this task's date is before the other's.
bool Task::DateIsBefore(const Task &task) const
{
	if(year != task.year)
		return (year < task.year);
	if(month != task.month)
		return (month < task.month);
	return (day < task.day);
}



// SearchText:
// Search the text of the task.
// If the pattern is found, this function returns true and sets the given
// selection to the range of character indices for the text.
bool Task::SearchText(const string &pattern, Selection &result, bool onlyMatchStart)
{
	// Convert both strings to lower case, in order to be case-insensitive.
	string lowPattern = ToLower(pattern);
	string lowText = ToLower(text);
	
	int length = static_cast<int>(pattern.length());
	
	// Figure out the range we're seatrching in. If we're only searching for strings
	// that start with the given pattern, the search range is limited to the first characters.
	string::iterator end = onlyMatchStart ? lowText.begin() + length : lowText.end();

	string::const_iterator found = search(lowText.begin(), end, pattern.begin(), pattern.end());
	if(found == end)
		return false;
	
	// If a match was found, update the selection to contain the matching substring.
	int first = static_cast<int>(found - lowText.begin());
	result.SetFirstAndLast(first, first + length);
	
	return true;
}



// CollatesBefore:
// Returns true if this string comes before the other in a case-insensitive lexical comparison.
bool Task::CollatesBefore(const Task &task)
{
	return ToLower(text) < ToLower(task.text);
}



// Date:
// Get the date of this task, formatted as a string.
string Task::Date() const
{
	ostringstream out;
	if(day != IGNORE_DATE)
		out << static_cast<int>(day) << ' ';
	if(month > 0 && month <= 12)
		out << MONTH[month - 1] << ' ';
	if(year != IGNORE_DATE)
		out << (1900 + year) << ' ';
	
	// Return the date without the trailing space.
	return out.str().substr(0, out.str().length() - 1);
}



// HasDate:
// Returns true if any portion of the date is specified.
bool Task::HasDate() const
{
	return (day != IGNORE_DATE) | (month != IGNORE_DATE) | (year != IGNORE_DATE);
}



// SetDate:
// Set the year, month, and day of this task's date.
void Task::SetDate(int year, int month, int day)
{
	this->year = year;
	this->month = month;
	this->day = day;
}



// SetDate:
// Read the given string and attempt to interpret it as a date.
// This is guranteed to work with any string created by Task::Date().
// It may not always interpret strings in other formats correctly.
void Task::SetDate(const string &date)
{
	ClearDate();

	if(date.empty())
		return;
	if(date == ".")
	{
		SetDateToToday();
		return;
	}
	
	istringstream in(date);
	string token;
	while(in >> token)
	{
		bool isNumeric = true;
		for(string::const_iterator it = token.begin(); it != token.end() && isNumeric; ++it)
			isNumeric &= (*it >= '0' && *it <= '9');
		if(token.length() >= 3 && !isNumeric)
		{
			// Check if this is a month.
			for(int i = 0; i < 12; ++i)
				if(ToLower(token[0]) == ToLower(MONTH[i][0]) &&
					ToLower(token[1]) == ToLower(MONTH[i][1]) &&
						ToLower(token[2]) == ToLower(MONTH[i][2]))
					month = i + 1;
			if(month == IGNORE_DATE)
			{
				ClearDate();
				return;
			}
		}
		else if(isNumeric)
		{
			int value = 0;
			istringstream parse(token);
			parse >> value;
			if(!value)
			{
				ClearDate();
				return;
			}
			if(value >= 1900)
				year = value - 1900;
			else if(month == IGNORE_DATE)
				day = value;
			else
				year = value + 100;
		}
	}
}



// SetDateToToday:
void Task::SetDateToToday()
{
	// Get the current date.
	time_t rawTime = time(NULL);
	tm &localTime = *localtime(&rawTime);
	
	year = localTime.tm_year;
	month = localTime.tm_mon + 1;
	day = localTime.tm_mday;
}



// ClearDate:
void Task::ClearDate()
{
	year = IGNORE_DATE;
	month = IGNORE_DATE;
	day = IGNORE_DATE;
}



// Text:
const string &Task::Text() const
{
	return text;
}



// SetText:
void Task::SetText(const string &text)
{
	this->text = text;
}



// Depth:
int Task::Depth() const
{
	return depth;
}



// SetDepth:
void Task::SetDepth(int depth)
{
	this->depth = depth;
}



// GetFlag:
// Get the value of the given flag (based on the constants defined in Task).
bool Task::GetFlag(unsigned short flag) const
{
	return ((flags & flag) != 0);
}



// SetFlag:
// Set the value of the given flag (based on the constants defined in Task).
void Task::SetFlag(unsigned short flag, bool set)
{
	if(set)
		flags |= flag;
	else
		flags &= ~flag;
}



// ToggleFlag:
// Toggle the state of the given flag.
void Task::ToggleFlag(unsigned short flag)
{
	flags ^= flag;
}



// GetFlagString:
// Convert the task's flags into a string.
// This is for writing the task in a semi-human-readable form.
string Task::GetFlagString() const
{
	string str;
	if(!IsRevealed())
		str += '#';
	str += IsDeleted() ? '{' : '[';
	if(IsEmphasized())
		str += str[str.length() - 1];
	str += IsChecked() ? 'x' : '_';
	str += IsDeleted() ? '}' : ']';
	if(IsEmphasized())
		str += str[str.length() - 1];

	return str;
}



// SetFlagString:
// Set the task's flags based on the given string, which should have been formatted
// using the GetFlagString() function above.
void Task::SetFlagString(const string &str)
{
	flags = 0;
	if(str.empty())
		return;
		
	if(str[0] != '#')
		flags |= REVEALED;
	if(str[str[0] == '#'] == '{')
		flags |= DELETED;
	if(str[str.size() / 2] == 'x')
		flags |= CHECKED;
	if(str.size() > 4)
		flags |= EMPHASIZED;
}



// operator>>:
// Read a task from the given input stream.
istream &operator>>(istream &in, Task &task)
{
	if(Task::READ_LEGACY)
	{
		// Old file format:
		task.depth = in.get();

		if(!in)
			task.depth = 0;
		else
		{
			task.flags = in.get();
			
			task.year = in.get();
			task.month = in.get();
			task.day = in.get();

			getline(in, task.text);
		}
	}
	else
	{
		task.depth = 0;
		task.flags = 0;
		task.ClearDate();
		
		// For simplicity's sake, first just read a whole line.
		string line;
		getline(in, line);
		if(!line.empty())
		{
			string::const_iterator it = line.begin();
			string::const_iterator end = line.end();
			// Strip trailing space.
			while(end > it)
			{
				if(*--end > ' ')
				{
					++end;
					break;
				}
			}
			for( ; it != end && *it < ' '; ++it)
				++task.depth;
			
			string::const_iterator textIt = it;
			do {
				if(it == end)
					break;
				if(*it == '#')
					++it;
				else
					task.flags |= Task::REVEALED;
				
				if(it == end)
					break;
				if(*it == '{')
					task.flags |= Task::DELETED;
				else if(*it != '[')
					break;
				++it;
				
				if(it == end)
					break;
				if(*it == '{' || *it == '[')
				{
					task.flags |= Task::EMPHASIZED;
					++it;
				}
				
				if(it == end)
					break;
				if(*it == 'x')
					task.flags |= Task::CHECKED;
				else if(*it != '_')
					break;
				++it;
				
				if(it == end || (*it != '}' && *it != ']'))
					break;
				++it;
				
				if(it == end || (task.IsEmphasized() && *it != '}' && *it != ']'))
					break;
				++it;
				
				if(it != end && *it == ' ')
					++it;
				
				textIt = it;
			} while(false);
			
			if(it != end)
			{
				// Check to see if the string ends with something in parentheses.
				string::const_iterator pend = end;
				while(pend != it && *--pend <= ' ') {}
				if(*pend == ')')
				{
					string::const_iterator pit = pend;
					while(pit != it && *--pit != '(') {}
					if(*pit == '(')
					{
						task.SetDate(string(pit + 1, pend));
						if(task.HasDate())
						{
							end = pit;
							// Skip trailing spaces.
							while(--end >= it)
								if(*end > ' ')
									break;
							++end;
						}
					}
				}
			}
			task.text.assign(it, end);
		}
	}
	
	return in;
}



// operator<<:
// Write a task to the given output stream.
ostream &operator<<(ostream &out, const Task &task)
{
	for(int i = 0; i < task.depth; ++i)
		out.put('\t');
	
	out << task.GetFlagString() << " ";
	
	out << task.text;
	
	if(task.HasDate())
		out << " (" << task.Date() << ")";
	
	out << endl;

	return out;
}



// ToLower:
// Convert a character to lower case.
char Task::ToLower(char c)
{
	return c | (((c >= 'A') & (c <= 'Z')) << 5);
}



// ToLower:
// Convert a string to lower case.
// This is to allow case-insensitive sorting and comparison.
string Task::ToLower(const string &s)
{
	string result = s;
	for(string::iterator it = result.begin(); it != result.end(); ++it)
		*it = ToLower(*it);
	return result;
}



/* Copyright (c) 2008 Michael Zahniser

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. */
